﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;

namespace Wicresoft.EKA.NRemedy
{

    /// <summary>
    /// http://www.cnblogs.com/JeffreyZhao/archive/2008/11/24/1338682.html
    /// </summary>
    public class DynamicMethodExecutor
    {
        private Func<object, object[], object> m_execute;

        public DynamicMethodExecutor(MethodInfo methodInfo)
        {
            this.m_execute = this.GetExecuteDelegate(methodInfo);
        }

        public object Execute(object instance, object[] parameters)
        {
            return this.m_execute(instance, parameters);
        }

        private Func<object, object[], object> GetExecuteDelegate(MethodInfo methodInfo)
        {
            // parameters to execute
            ParameterExpression instanceParameter =
                Expression.Parameter(typeof(object), "instance");
            ParameterExpression parametersParameter =
                Expression.Parameter(typeof(object[]), "parameters");

            // build parameter list
            List<Expression> parameterExpressions = new List<Expression>();
            ParameterInfo[] paramInfos = methodInfo.GetParameters();
            for (int i = 0; i < paramInfos.Length; i++)
            {
                // (Ti)parameters[i]
                BinaryExpression valueObj = Expression.ArrayIndex(
                    parametersParameter, Expression.Constant(i));
                UnaryExpression valueCast = Expression.Convert(
                    valueObj, paramInfos[i].ParameterType);

                parameterExpressions.Add(valueCast);
            }

            // non-instance for static method, or ((TInstance)instance)
            Expression instanceCast = methodInfo.IsStatic ? null :
                Expression.Convert(instanceParameter, methodInfo.ReflectedType);

            // static invoke or ((TInstance)instance).Method
            MethodCallExpression methodCall = Expression.Call(
                instanceCast, methodInfo, parameterExpressions);

            // ((TInstance)instance).Method((T0)parameters[0], (T1)parameters[1], ...)
            if (methodCall.Type == typeof(void))
            {
                Expression<Action<object, object[]>> lambda =
                    Expression.Lambda<Action<object, object[]>>(
                        methodCall, instanceParameter, parametersParameter);

                Action<object, object[]> execute = lambda.Compile();
                return (instance, parameters) =>
                {
                    execute(instance, parameters);
                    return null;
                };
            }
            else
            {
                UnaryExpression castMethodCall = Expression.Convert(
                    methodCall, typeof(object));
                Expression<Func<object, object[], object>> lambda =
                    Expression.Lambda<Func<object, object[], object>>(
                        castMethodCall, instanceParameter, parametersParameter);

                return lambda.Compile();
            }
        }
    }

    public class PropertyInfoAndARAttribute
    {
        public PropertyInfo Property { get; set; }
        public ARFieldAttribute ARAttribute { get; set; }
        public DynamicMethodExecutor FastInvoker { get; set; }
    }


    public class BinderHelper
    {
        private static Dictionary<string, List<PropertyInfoAndARAttribute>> _maps = new Dictionary<string, List<PropertyInfoAndARAttribute>>();


        /// <summary>
        /// 
        /// </summary>
        /// <param name="mType"></param>
        /// <param name="IsBinder">is the ar convert to the .net</param>
        /// <returns></returns>
        public static string GetClassName(Type mType, bool IsBinder)
        {
            var keyName = "Copy_";
            keyName += mType.FullName.Replace(".", "_");
            keyName += "Binder";
            if (IsBinder)
            {
                keyName += "_TypeIsBinder";
            }
            else
            {
                keyName += "_TypeIsUnBinder";
            }
            return keyName;
        }

        public static List<PropertyInfoAndARAttribute> GetPropertyInfos(Type mType, bool IsBinder)
        {
            List<PropertyInfoAndARAttribute> properties;
            string name = GetClassName(mType, IsBinder);
            if (_maps.TryGetValue(name, out properties)) return properties;
            lock (_maps)
            {
                ARFieldAttribute fieldAttribute = null;

                PropertyInfo[] propertiesTemp;
                if (!_maps.TryGetValue(name, out properties))
                {
                    properties = new List<PropertyInfoAndARAttribute>();
                    if (IsBinder)
                    {
                        propertiesTemp = mType.GetProperties
                            //binder need at least writeable property
                (BindingFlags.SetProperty | BindingFlags.Public | BindingFlags.Instance);
                        foreach (var item in propertiesTemp)
                        {
                            fieldAttribute = GetARAttributeFieldForProperty(item, ModelBinderAccessLevel.OnlyBind);
                            if (fieldAttribute != null)
                            {
                                ARType dataType = fieldAttribute.DataType;
                                if (dataType == ARType.None) throw new ArgumentOutOfRangeException("ARType");
                                if (properties.Where(o => o.ARAttribute.DatabaseID == fieldAttribute.DatabaseID).Count() > 0) throw new DuplicateIDException(fieldAttribute.DatabaseID);
                                if ((item.PropertyType.IsEnum && dataType != ARType.SelectionField) || (!item.PropertyType.IsEnum && dataType == ARType.SelectionField))
                                    throw new ModelException("Selection Field should be the Enum,And Enum should be the Selection Field");

                                properties.Add(
                                   new PropertyInfoAndARAttribute
                                   {
                                       Property = item,
                                       ARAttribute = fieldAttribute,
                                       FastInvoker = new DynamicMethodExecutor(item.GetSetMethod())
                                   });
                            }
                        }
                    }
                    else
                    {
                        propertiesTemp = mType.GetProperties
                            //unbinder need at least readable property
                (BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.Instance);
                        foreach (var item in propertiesTemp)
                        {
                            fieldAttribute = GetARAttributeFieldForProperty(item, ModelBinderAccessLevel.OnlyUnBind);
                            if (fieldAttribute != null)
                            {
                                ARType dataType = fieldAttribute.DataType;
                                if (dataType == ARType.None) throw new ArgumentOutOfRangeException("ARType");
                                if (properties.Where(o => o.ARAttribute.DatabaseID == fieldAttribute.DatabaseID).Count() > 0) throw new DuplicateIDException(fieldAttribute.DatabaseID);
                                if ((item.PropertyType.IsEnum && dataType != ARType.SelectionField) || (!item.PropertyType.IsEnum && dataType == ARType.SelectionField))
                                    throw new ModelException("Selection Field should be the Enum,And Enum should be the Selection Field");
                                properties.Add(
                                    new PropertyInfoAndARAttribute
                                    {
                                        Property = item,
                                        ARAttribute = fieldAttribute,
                                        FastInvoker = new DynamicMethodExecutor(item.GetGetMethod())
                                    });
                            }
                        }
                    }
                    _maps.Add(name, properties);
                }
            }
            return properties;
        }


        public static ARFieldAttribute GetARAttributeFieldForProperty(PropertyInfo pi, ModelBinderAccessLevel accessLevel)
        {
            if (pi == null)
                throw new ArgumentNullException("pi");
            var attrs = pi.GetCustomAttributes(typeof(ARFieldAttribute), false);
            if (attrs.Length > 1)
                throw new CustomAttributeFormatException(
                    string.Format("Mutiple ARFieldAttribute is found on Property : {0}.", pi.Name));
            if (attrs.Length == 0) return null;
            ARFieldAttribute attribute = attrs.First() as ARFieldAttribute;
            if (attribute.DatabaseID == 0)
                throw new CustomAttributeFormatException(
                    string.Format("DatabaseID of ARFieldAttribute on Property : {0} is missing.", pi.Name));
            if (attribute.DataType == ARType.None)
                throw new CustomAttributeFormatException(
                    string.Format("DataType of ARFieldAttribute on Property : {0} cannot be null.", pi.Name));
            if ((attribute.BinderAccess & accessLevel) == accessLevel)
                return attribute;
            else
                return null;
        }

    }
}
